20230816-154552

Python中的装饰器

Python中一般提起装饰器,可能最先想到的就是@,这本身是一个语法糖func = wrapper(func)的简介表示。下面将简单介绍下,顺带举几个实用的例子便于自身理解。

闭包

我个人感觉闭包这个概念是变量作用域的特殊称呼,也就是在函数嵌套定义过程中不同函数作用域的范围,下面还是通过代码来演示:

def startAt(x):
	def incrementBy(y):
		return x + y
	return incrementBy

a = startAt(1)
print(f"function: {a}") # <function incrementBy at 0x >
print(f"result: {a(1)}") # 2
# 这里其实就有一个神奇的地方了,当我们调用startAt(1)时,传入的x变量,按理来说已经结束了,该被销毁,但是在后续继续调用其的返回函数时,仍然可以运行,并且“记住了”我们前面传递的x值。

从上述的代码就可以看出,函数里面嵌套定义函数的方式,会把内嵌函数定义的环境(也即,上下文Context)保存下来,这样即使外面的函数已经执行完毕,内嵌的闭包函数依然能够运行。可以利用这一种机制来记录一些信息,下面这段代码,我感觉还是很巧妙的:

def create(pos=(0, 0)):
    def go(direction, step):
		# 这里由于要修改外部函数的局部变量,因此需要nonlocal声明。这里也需要注意:如果内部变量和外部函数的局部变量相同,则会优先使用内部变量。当然也可以使用nonlocal声明,来使用外部函数的局部变量
        nonlocal pos
        new_x = pos[0] + direction[0] * step
        new_y = pos[1] + direction[1] * step
        pos = (new_x, new_y)
        return pos
    return go

player = create()
print(f"1st step: {player([1, 0], 10)}") # (10, 0)
print(f"2nd step: {player([0, 1], 20)}") # (10, 20)
print(f"3rd step: {player([-1, 1], 5)}") # (5, 25)

装饰器

其实,我感觉闭包和装饰器之间的出发思路是不太一样的,但是其都具有嵌套函数定义的形式,因此大多想把装饰器讲得深一点的文章,都会选择从闭包入手,所以这里也就随个波。

我个人感觉装饰器,本身的目的是为了减少代码改动,把一些非内聚的功能独立出去,便于维护,由于引入了@的语法糖,当然看起来也更好看。下面讲2个装饰器的例子:

无参数版

def decorator(func):
	# 此处好多装饰器文章里,都喜欢写成这种全捕捉的参数传入,我不是很理解,下面再详细说明
	def wrapper(*args, **kwargs):
		print('in wrapper')
		return func(*args, **kwargs)
	return wrapper

@decorator
def funcA(x):
	print(f"we are in funcA, and x={x}")

print(funcA) # <function decorator.<locals>.wrapper at 0x7f301a284f40>

# 其实上述的语法糖可以拆开, funcA = decorator(funcA),因此经过装饰器的修饰,funcA此时已经是wrapper函数了。

通过对上述代码的运行和结果,可以得到经过装饰器修饰后,函数已经被替换为wrapper了。这里有一点:wrapper能够捕捉任意的传入参数,是不是经过替换,这样的调用funcA(1, 2)是可以的呢?结果很遗憾并不行,这里就需要用到上面闭包的知识了,虽然我们原始的funcA已经被替换,但是由于是在外部函数引入的,其作为上下文,被保存了下来,因此此时仍然只能向funcA传递一个位置参数。当然我们可以用下面的代码,绕过这种限制,但是正如我在注释中写的,我感觉这种捕捉全部参数的写法不太好,容易造成误解,不如直接把wrapper的传入参数也定义为1个,同funcA保持一直!

def decorator(func):
	def wrapper(*args, **kwargs):
		print('in wrapper')
		# 哈哈哈哈哈,这里直接就取一个,其他参数全部丢弃
		return func(args[0])
	return wrapper

带参数版

这里直接上代码,取自一个实际应用案例:

def retry(times, interval):
    def decorator(f):
        def wrapper(*args, **kwargs):
            nonlocal times
            while times:
                try:
                    #print('suceeded')
                    return f(*args, **kwargs)
                except MPRestError as ex:
                    if re.search(r"NoneType", str(ex)):
                        print("no results return")
                        return None
                    if re.search(r"No result for record", str(ex)):
                        print(f"no results return for {args[1]}")
                        return {}
                    else:
                        times -= 1
                        print(f'retrying {times} times.')
                        time.sleep(interval)
                        continue
        return wrap
    return decorator

@retry(5, 10)
def download(url):
	return r.get(url)

本质上,这里的装饰器比无参数版只深了一小步,@retry(5, 10)在程序运行过程中,应该首先执行的是retry(5, 10),这里返回的是一个函数decorator,是不是就和上面的无参数版的一致了!当然这里传入的参数times和interval依然是作为上下文被储存起来,可以在 真正功能实现的wrapper里使用。当然这个例子里面对nonlocal的使用和最开始闭包的例子有点相像,这里也不再赘述。

装饰器类装饰类时的执行顺序

其实这才是一开始实际工作中遇到的问题,其中先后执行顺序的模糊,导致写的代码不能work,因此写了这篇和Python中的元类两篇记录。也算是对自己Python只是的一点补充!